# -*- coding: utf-8 -*-
# Copyright (c) 2004 Divmod.
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

__all__ = ('Expose',)  # @todo: ...

import os
import inspect
import codecs
import time

import eof.log  as log
import eof.util as U
import eof.ztk  as ztk

from eof.txbr import ifaces

#import twisted.internet.task as twtask
#from twisted.python import compat
#from . import inevow


class UnexposedMethodError(Exception):
    """Raised on any attempt to get a method which has not been exposed."""


def networkString(obj):
    if not isinstance(obj, str):
        raise TypeError('Can only convert text to bytes on Python 3')
    return obj.encode('ascii')


class Expose:
    """
    Helper for exposing methods for various uses using a simple decorator-style
    callable.

    Instances of this class can be called with one or more functions as
    positional arguments.  The names of these functions will be added to a list
    on the class object of which they are methods.

    @ivar attributeName: The attribute with which exposed methods will be
    tracked.
    """
    def __init__(self, doc=None):
        self.doc = doc

    def __call__(self, *funcObjs):
        """
        Add one or more functions to the set of exposed functions.

        This is a way to declare something about a class definition, similar to
        L{zope.interface.implements}.  Use it like this::

        | magic = Expose('perform extra magic')
        | class Foo(Bar):
        |     def twiddle(self, x, y):
        |         ...
        |     def frob(self, a, b):
        |         ...
        |     magic(twiddle, frob)

        Later you can query the object::

        | aFoo = Foo()
        | magic.get(aFoo, 'twiddle')(x=1, y=2)

        The call to C{get} will fail if the name it is given has not been
        exposed using C{magic}.

        @param funcObjs: One or more function objects which will be exposed to
        the client.

        @return: The first of C{funcObjs}.
        """
        if not funcObjs:
            raise TypeError("expose() takes at least 1 argument (0 given)")
        for fObj in funcObjs:
            fObj.exposedThrough = getattr(fObj, 'exposedThrough', [])
            fObj.exposedThrough.append(self)
        return funcObjs[0]

    def exposedMethodNames(self, instance):
        """
        Return an iterator of the names of the methods which are exposed on the
        given instance.
        """
        for k, callable in inspect.getmembers(instance, inspect.isroutine):
            if self in getattr(callable, 'exposedThrough', []):
                yield k

    _nodefault = object()
    def get(self, instance, methodName, default=_nodefault):
        """
        Retrieve an exposed method with the given name from the given instance.

        @raise UnexposedMethodError: Raised if C{default} is not specified and
        there is no exposed method with the given name.

        @return: A callable object for the named method assigned to the given
        instance.
        """
        method = getattr(instance, methodName, None)
        exposedThrough = getattr(method, 'exposedThrough', [])
        if self not in getattr(method, 'exposedThrough', []):
            if default is self._nodefault:
                raise UnexposedMethodError(self, methodName)
            return default
        return method


def escapeToXML(text, isattrib = False):
    """Borrowed from twisted.xish.domish

    Escape text to proper XML form, per section 2.3 in the XML specification.

     @type text: L{str}
     @param text: Text to escape

     @type isattrib: L{bool}
     @param isattrib: Triggers escaping of characters necessary for use as attribute values
    """
    text = text.replace("&", "&amp;")
    text = text.replace("<", "&lt;")
    text = text.replace(">", "&gt;")
    if isattrib:
        text = text.replace("'", "&apos;")
        text = text.replace("\"", "&quot;")
    return text


def getPOSTCharset(ctx):
    """Locate the unicode encoding of the POST'ed form data.

    To work reliably you must do the following:

      - set the form's enctype attribute to 'multipart/form-data'
      - set the form's accept-charset attribute, probably to 'utf-8'
      - add a hidden form field called '_charset_'

    For instance::

      <form action="foo" method="post" enctype="multipart/form-data" accept-charset="utf-8">
        <input type="hidden" name="_charset_" />
        ...
      </form>
    """

    request = inevow.IRequest(ctx)

    # Try the magic '_charset_' field, Mozilla and IE set this.
    charset = request.args.get('_charset_',[None])[0]
    if charset:
        return charset

    # Look in the 'content-type' request header
    contentType = request.received_headers.get('content-type')
    if contentType:
        charset = dict([ s.strip().split('=') for s in contentType.split(';')[1:] ]).get('charset')
        if charset:
            return charset

    return 'utf-8'


def getCodecFromContentType(headers, default='utf-8'):
    for ct in headers.getRawHeaders(b'content-type', []):
        for param in ct.lower().split(b';'):
            if b'=' in param:
                key, value = map(bytes.strip, param.split(b'=', 1))
                if key == b'charset':
                    charset = value.decode('ascii')
                    break
        else:
            continue
        break
    else:
        charset = default

    if charset != default:
        try:
            return codecs.lookup(charset)
        except LookupError:
            log.failure('Invalid charset "{charset}", will be used "{default}"', charset=charset, default=default)

    return codecs.lookup(default)


## The tests rely on these, but they should be removed ASAP
def remainingSegmentsFactory(ctx):
    return tuple(ctx.tag.postpath)


def currentSegmentsFactory(ctx):
    return tuple(ctx.tag.prepath)


class _RandomClazz:
    pass


class _NamedAnyError(Exception):
    'Internal error for when importing fails.'


def _namedAnyWithBuiltinTranslation(name):
    if name == 'builtins.function':
        name='types.FunctionType'
    elif name == 'builtins.method':
        return _RandomClazz # Hack
    elif name == 'builtins.instancemethod':
        name='types.MethodType'
    elif name == 'builtins.NoneType':
        return type(None)
    elif name == 'builtins.generator':
        name='types.GeneratorType'
    return U.namedAny(name)


class CachedFile:
    """
    Helper for caching operations on files in the filesystem.
    """
    def __init__(self, path, loader):
        """
        @type path: L{str}
        @param path: The path to the associated file in the filesystem.

        @param loader: A callable that returns the relevant data; invoked when
        the cache is empty or stale.
        """

        self.path = path
        self.loader = loader
        self.invalidate()

    def invalidate(self):
        """
        Invalidate the cache, forcing a reload from disk at the next attempted
        load.
        """
        self._mtime = None

    def load(self, *args, **kwargs):
        """
        Load this file. Any positional or keyword arguments will be passed
        along to the loader callable, after the path itself.
        """
        currentTime = os.path.getmtime(self.path)
        if self._mtime is None or currentTime != self._mtime:
            self._cachedObj = self.loader(self.path, *args, **kwargs)
            self._mtime = currentTime

        return self._cachedObj


def redirectTo(URL, request):
    """
    Generate a redirect to the given location.

    @param URL: A L{bytes} giving the location to which to redirect.
    @type URL: L{bytes}

    @param request: The request object to use to generate the redirect.
    @type request: L{IRequest<twisted.web.iweb.IRequest>} provider

    @raise TypeError: If the type of C{URL} a L{unicode} instead of L{bytes}.

    @return: A C{bytes} containing HTML which tries to convince the client agent
        to visit the new location even if it doesn't respect the I{FOUND}
        response code.  This is intended to be returned from a render method,
        eg::

            def render_GET(self, request):
                return redirectTo(b"http://example.com/", request)
    """

    if isinstance(URL, str):
        raise TypeError('<str> object not allowed as URL')

    request.setHeader(b'Content-Type', b'text/html; charset=utf-8')
    request.redirect(URL)

    content = '''
<html>
    <head>
        <meta http-equiv="refresh" content="0;URL=%(url)s">
    </head>
    <body bgcolor="#FFFFFF" text="#000000">
    <a href="{url}">click here</a>
    </body>
</html>
'''.format(url=U.strictstr(URL, 'utf-8'))

    return content.encode('utf8')


def datetimeToString(msSinceEpoch=None):
    """
    Convert seconds since epoch to HTTP datetime string.

    @rtype: C{bytes}
    """

    if msSinceEpoch == None:
        msSinceEpoch = time.time()

    year, month, day, hh, mm, ss, wd, y, z = time.gmtime(msSinceEpoch)

    return networkString('%s, %02d %3s %4d %02d:%02d:%02d GMT' % (
        weekdayname[wd],
        day, monthname[month], year,
        hh, mm, ss
    ))


@ztk.implementer(ifaces.IPushProducer)
class PullToPush:
    """
    An adapter that converts a non-streaming to a streaming producer.

    Because of limitations of the producer API, this adapter requires the
    cooperation of the consumer. When the consumer's C{registerProducer} is
    called with a non-streaming producer, it must wrap it with L{_PullToPush}
    and then call C{startStreaming} on the resulting object. When the
    consumer's C{unregisterProducer} is called, it must call
    C{stopStreaming} on the L{_PullToPush} instance.

    If the underlying producer throws an exception from C{resumeProducing},
    the producer will be unregistered from the consumer.

    @ivar _producer: the underling non-streaming producer.

    @ivar _consumer: the consumer with which the underlying producer was
                     registered.

    @ivar _finished: C{bool} indicating whether the producer has finished.

    @ivar _coopTask: the result of calling L{twtask.cooperate}, the task driving the
                     streaming producer.
    """

    _finished = False

    def __init__(self, pullProducer, consumer):
        self._producer = pullProducer
        self._consumer = consumer

    def _pull(self):
        """
        A generator that calls C{resumeProducing} on the underlying producer
        forever.

        If C{resumeProducing} throws an exception, the producer is
        unregistered, which should result in streaming stopping.
        """
        while True:
            try:
                self._producer.resumeProducing()
            except Exception:
                log.failure('{producer!r} failed, producing will be stopped', producer=U.safestr(self._producer))
                try:
                    self._consumer.unregisterProducer()
                    # The consumer should now call stopStreaming() on us,
                    # thus stopping the streaming.
                except Exception:
                    # Since the consumer blew up, we may not have had
                    # stopStreaming() called, so we just stop on our own:
                    log.failure('{consumer!r} failed to unregister producer', consumer=U.safestr(self._consumer))
                    self._finished = True
                    return
            yield None

    def startStreaming(self):
        """This should be called by the consumer when the producer is registered.

        Start streaming data to the consumer."""

        self._coopTask = twtask.cooperate(self._pull())

    def stopStreaming(self):
        """
        This should be called by the consumer when the producer is
        unregistered.

        Stop streaming data to the consumer.
        """
        if not self._finished:
            self._finished = True
            self._coopTask.stop()

    def pauseProducing(self):
        """@see: C{ifaces.IPushProducer.pauseProducing}"""
        self._coopTask.pause()

    def resumeProducing(self):
        """@see: C{ifaces.IPushProducer.resumeProducing}"""
        self._coopTask.resume()

    def stopProducing(self):
        """@see: C{ifaces.IPushProducer.stopProducing}"""
        self.stopStreaming()
        self._producer.stopProducing()


def proxyForInterface(iface, originalAttribute='original'):
    """
    Create a class which proxies all method calls which adhere to an interface
    to another provider of that interface.

    This function is intended for creating specialized proxies. The typical way
    to use it is by subclassing the result::

      class MySpecializedProxy(proxyForInterface(IFoo)):
          def someInterfaceMethod(self, arg):
              if arg == 3:
                  return 3
              return self.original.someInterfaceMethod(arg)

    @param iface: The Interface to which the resulting object will conform, and
        which the wrapped object must provide.

    @param originalAttribute: name of the attribute used to save the original
        object in the resulting class. Default to C{original}.
    @type originalAttribute: C{str}

    @return: A class whose constructor takes the original object as its only
        argument. Constructing the class creates the proxy.
    """

    def __init__(self, original):
        setattr(self, originalAttribute, original)

    contents = {'__init__': __init__}  # type: Dict[str, object]

    for name in iface:
        contents[name] = _ProxyDescriptor(name, originalAttribute)

    proxy = type('(Proxy for {})'.format(U.qual(iface)), (object,), contents)
    ztk.classImplements(proxy, iface)

    return proxy


class _ProxyDescriptor:
    """
    A descriptor which will proxy attribute access, mutation, and
    deletion to the L{_ProxyDescriptor.originalAttribute} of the
    object it is being accessed from.

    @ivar attributeName: the name of the attribute which this descriptor will
        retrieve from instances' C{original} attribute.
    @type attributeName: C{str}

    @ivar originalAttribute: name of the attribute of the proxy where the
        original object is stored.
    @type originalAttribute: C{str}
    """

    def __init__(self, attributeName, originalAttribute):
        self.attributeName = attributeName
        self.originalAttribute = originalAttribute

    def __get__(self, oself, type=None):
        """
        Retrieve the C{self.attributeName} property from I{oself}.
        """
        if oself is None:
            return _ProxiedClassMethod(self.attributeName, self.originalAttribute)
        original = getattr(oself, self.originalAttribute)
        return getattr(original, self.attributeName)

    def __set__(self, oself, value):
        """
        Set the C{self.attributeName} property of I{oself}.
        """
        original = getattr(oself, self.originalAttribute)
        setattr(original, self.attributeName, value)

    def __delete__(self, oself):
        """
        Delete the C{self.attributeName} property of I{oself}.
        """
        original = getattr(oself, self.originalAttribute)
        delattr(original, self.attributeName)


class _ProxiedClassMethod:
    """
    A proxied class method.

    @ivar methodName: the name of the method which this should invoke when
        called.
    @type methodName: L{str}

    @ivar __name__: The name of the method being proxied (the same as
        C{methodName}).
    @type __name__: L{str}

    @ivar originalAttribute: name of the attribute of the proxy where the
        original object is stored.
    @type originalAttribute: L{str}
    """

    def __init__(self, methodName, originalAttribute):
        self.methodName = self.__name__ = methodName
        self.originalAttribute = originalAttribute

    def __call__(self, oself, *args, **kw):
        """
        Invoke the specified L{methodName} method of the C{original} attribute
        for proxyForInterface.

        @param oself: an instance of a L{proxyForInterface} object.

        @return: the result of the underlying method.
        """
        original = getattr(oself, self.originalAttribute)
        actualMethod = getattr(original, self.methodName)
        return actualMethod(*args, **kw)
